/**
 * 
 */
package network.internal.communication;

import static commons.Preconditions.checkIsValidPort;
import static commons.Preconditions.checkNotNull;

import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.util.LinkedList;
import java.util.List;

import javax.net.SocketFactory;

import network.communication.Connection;
import network.communication.ConnectionFactory;
import network.communication.ServerProtocol;
import network.internal.Message;

import commons.exception.AlreadyUsedNameException;
import commons.exception.AttributeNotFoundException;
import commons.exception.ClosedConnectionException;
import commons.exception.NotFoundNameException;
import commons.operation.Operation;
import commons.operation.OperationParameter;

/**
 * @author armstrong
 * 
 */
public class DefaultServerProtocol implements ServerProtocol {

	private final String			serverHostName;
	private final int				serverPort;
	private Connection				connection;
	private final SocketFactory		socketFactory;
	private final ConnectionFactory	connectionFactory;

	/**
	 * @throws IOException
	 * @throws UnknownHostException
	 * 
	 */
	public DefaultServerProtocol(final String serverHostName,
									final int serverPort,
									final SocketFactory socketFactory,
									final ConnectionFactory connectionFactory)
			throws UnknownHostException, IOException {

		checkNotNull("serverHostName cannot be null.", serverHostName);
		checkIsValidPort("serverPort invalid", serverPort);
		checkNotNull("socketFactory cannot be null.", socketFactory);
		checkNotNull("connectionFactory cannot be null.", connectionFactory);

		this.serverHostName = serverHostName;
		this.serverPort = serverPort;
		this.socketFactory = socketFactory;
		this.connectionFactory = connectionFactory;
	}

	@Override
	public void close() throws IOException, ClosedConnectionException {
		connection.close();
	}

	private void create(final Operation operation) throws IOException,
			AlreadyUsedNameException, ClosedConnectionException {
		final Socket socketToServer = socketFactory.createSocket(
				serverHostName, serverPort);
		connection = connectionFactory.createConnection(socketToServer);

		connection.writeByte((byte) Message.CreateFile.ordinal());
		connection.writeString((String) operation
				.getParameter(OperationParameter.FileName));

		final Byte status = connection.readByte();

		if (status == Message.FileAlreadyExist.ordinal()) {
			throw new AlreadyUsedNameException();
		}
	}

	private void delete(final Operation operation) throws IOException,
			NotFoundNameException, ClosedConnectionException {
		final Socket socketToServer = socketFactory.createSocket(
				serverHostName, serverPort);
		connection = connectionFactory.createConnection(socketToServer);

		connection.writeByte((byte) Message.DeleteFile.ordinal());
		connection.writeString((String) operation
				.getParameter(OperationParameter.FileName));

		final Byte status = connection.readByte();

		if (status == Message.FileDoesNotExist.ordinal()) {
			throw new NotFoundNameException();
		}
	}

	private String getAttribute(final Operation operation) throws IOException,
			NotFoundNameException, AttributeNotFoundException,
			ClosedConnectionException {

		final Socket socketToServer = socketFactory.createSocket(
				serverHostName, serverPort);
		connection = connectionFactory.createConnection(socketToServer);

		connection.writeByte((byte) Message.GetAttribute.ordinal());
		connection.writeString((String) operation
				.getParameter(OperationParameter.FileName));
		connection.writeString((String) operation
				.getParameter(OperationParameter.AttributeName));

		final Byte status = connection.readByte();

		if (status == Message.FileDoesNotExist.ordinal()) {
			throw new NotFoundNameException();
		} else if (status == Message.AttributeDoesNotExist.ordinal()) {
			throw new AttributeNotFoundException();
		}

		return connection.readString();
	}

	// TODO Maybe these private methods should receive only the operation
	// arguments, not the whole operation.
	// FIXME these private methods should create the connection instead of
	// receiving as an argument

	@Override
	public String getHostname() {
		return serverHostName;
	}

	@Override
	public int getPort() {
		return serverPort;
	}

	private void handleException(final Exception thrownException,
			final Connection connection) throws Exception {
		if (thrownException != null) {
			if (thrownException instanceof AlreadyUsedNameException) {
				throw thrownException;
			} else if (thrownException instanceof NotFoundNameException) {
				throw thrownException;
				// maybe make IOException encapsulate the exceptions above
			} else if (thrownException instanceof IOException) {
				throw thrownException;
			} else if (thrownException instanceof AttributeNotFoundException) {
				throw thrownException;
			} else {
				throw thrownException;
			}
		}
	}

	private List<String> list(final Operation operation)
			throws NumberFormatException, IOException,
			ClosedConnectionException {

		final Socket socketToServer = socketFactory.createSocket(
				serverHostName, serverPort);
		connection = connectionFactory.createConnection(socketToServer);

		connection.writeByte((byte) Message.List.ordinal());

		// Reads the status byte
		// Currently the ServerHandler is writing only StatusOk
		// as response, so it has no use here
		connection.readByte();
		// FIXME maybe encapsulate this conversion to Number (Integer or Long)
		// in Connection. Connection will try to convert the string and throw
		// IOException in case of NumberFormatException
		final Integer listSize = new Integer(connection.readString());

		final List<String> list = new LinkedList<String>();
		for (int i = 0; i < listSize; i++) {
			list.add(connection.readString());
		}

		return list;
	}

	@Override
	public Object operateOnServer(final Operation operation) throws Exception {

		checkNotNull("operation cannot be null.", operation);

		Object result = null;
		Exception thrownException = null;

		try {
			switch (operation.type()) {
				case CreateFile:
					create(operation);
					break;
				case DeleteFile:
					delete(operation);
					break;
				case List:
					result = list(operation);
					break;
				case Size:
					result = size(operation);
					break;
				case SetAttribute:
					setAttribute(operation);
					break;
				case GetAttribute:
					result = getAttribute(operation);
					break;
				case SetSize:
					setSize(operation);
					break;
				case Write:
					write(operation);
					break;
				case Read:
					result = read(operation);
					break;
				default:
					throw new UnsupportedOperationException();
			}
		} catch (final Exception e) {
			thrownException = e;
		}

		handleException(thrownException, connection);

		return result;
	}

	private Integer read(final Operation operation) throws IOException,
			NotFoundNameException, ClosedConnectionException {
		final Socket socketToServer = socketFactory.createSocket(
				serverHostName, serverPort);
		connection = connectionFactory.createConnection(socketToServer);

		final ByteBuffer bufferResponse = (ByteBuffer) operation
				.getParameter(OperationParameter.ByteBuffer);

		connection.writeByte((byte) Message.Read.ordinal());
		connection.writeString((String) operation
				.getParameter(OperationParameter.FileName));
		connection.writeString((String) operation
				.getParameter(OperationParameter.DataAmount));
		connection.writeString((String) operation
				.getParameter(OperationParameter.Position));

		final Byte status = connection.readByte();

		if (status == Message.FileDoesNotExist.ordinal()) {
			throw new NotFoundNameException();
		}

		Integer readResponse = 0;
		final Integer totalRead = new Integer(connection.readString());
		String readData = "";

		// FIXME possibly a infinite loop
		while (readResponse < totalRead) {
			try {
				// FIXME maybe there is a better solution than
				// to use a string
				final ByteBuffer buffer = connection.readByteBuffer();
				readData += new String(buffer.array());
				readResponse += buffer.capacity();
			} catch (final IOException e) {
				e.printStackTrace();
			}
		}
		bufferResponse.put(readData.getBytes());
		return totalRead;
	}

	private void setAttribute(final Operation operation) throws IOException,
			NotFoundNameException, ClosedConnectionException {

		final Socket socketToServer = socketFactory.createSocket(
				serverHostName, serverPort);
		connection = connectionFactory.createConnection(socketToServer);

		connection.writeByte((byte) Message.SetAttribute.ordinal());
		connection.writeString((String) operation
				.getParameter(OperationParameter.FileName));
		connection.writeString((String) operation
				.getParameter(OperationParameter.AttributeName));
		connection.writeString((String) operation
				.getParameter(OperationParameter.AttributeValue));

		final Byte status = connection.readByte();

		if (status == Message.FileDoesNotExist.ordinal()) {
			throw new NotFoundNameException();
		}
	}

	private void setSize(final Operation operation) throws IOException,
			NotFoundNameException, ClosedConnectionException {

		final Socket socketToServer = socketFactory.createSocket(
				serverHostName, serverPort);
		connection = connectionFactory.createConnection(socketToServer);

		connection.writeByte((byte) Message.SetSize.ordinal());
		connection.writeString((String) operation
				.getParameter(OperationParameter.FileName));
		connection.writeString((String) operation
				.getParameter(OperationParameter.Size));

		final Byte status = connection.readByte();

		if (status == Message.FileDoesNotExist.ordinal()) {
			throw new NotFoundNameException();
		}
	}

	private Long size(final Operation operation) throws IOException,
			NotFoundNameException, ClosedConnectionException {

		final Socket socketToServer = socketFactory.createSocket(
				serverHostName, serverPort);
		connection = connectionFactory.createConnection(socketToServer);

		connection.writeByte((byte) Message.Size.ordinal());
		connection.writeString((String) operation
				.getParameter(OperationParameter.FileName));

		final Byte status = connection.readByte();

		if (status == Message.FileDoesNotExist.ordinal()) {
			throw new NotFoundNameException();
		}

		return new Long(connection.readString());
	}

	private void write(final Operation operation) throws IOException,
			NotFoundNameException, ClosedConnectionException {

		final Socket socketToServer = socketFactory.createSocket(
				serverHostName, serverPort);
		connection = connectionFactory.createConnection(socketToServer);

		connection.writeByte((byte) Message.Write.ordinal());
		connection.writeString((String) operation
				.getParameter(OperationParameter.FileName));
		connection.writeString((String) operation
				.getParameter(OperationParameter.DataAmount));
		connection.writeString((String) operation
				.getParameter(OperationParameter.Position));
		connection.writeByteBuffer((ByteBuffer) operation
				.getParameter(OperationParameter.ByteBuffer));

		final Byte status = connection.readByte();

		if (status == Message.FileDoesNotExist.ordinal()) {
			throw new NotFoundNameException();
		}
	}
}
